﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

namespace FinMkts.Prices.ClientService
{
    /// <summary>
    /// Maintains a list of subscriptions for sessions and topics
    /// </summary>
    internal class Subscriptions
    {
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
        private readonly Dictionary<string, HashSet<Subscription>> _bySession = new Dictionary<string, HashSet<Subscription>>();
        private readonly Dictionary<string, HashSet<Subscription>> _byTopic = new Dictionary<string, HashSet<Subscription>>();

        /// <summary>
        /// Raised when the first subscription for a topic is added
        /// </summary>
        public event EventHandler<SubscriptionEventArgs> TopicAdded;

        /// <summary>
        /// Raised when the last subscription for a topic is removed
        /// </summary>
        public event EventHandler<SubscriptionEventArgs> TopicRemoved;

        /// <summary>
        /// Get all topics with at least one subscription
        /// </summary>
        /// <returns></returns>
        public HashSet<string> GetTopics()
        {
            _lock.EnterReadLock();
            try
            {
                return new HashSet<string>(_byTopic.Keys);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// Get all subscriptions for a topic
        /// </summary>
        /// <param name="topic"></param>
        /// <returns></returns>
        public HashSet<Subscription> GetSubscriptionsByTopic(string topic)
        {
            _lock.EnterReadLock();
            try
            {
                return GetSubscriptions(_byTopic, topic);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// Get all subscriptions for a session
        /// </summary>
        /// <param name="sessionId"></param>
        /// <returns></returns>
        public HashSet<Subscription> GetSubscriptionsBySession(string sessionId)
        {
            _lock.EnterReadLock();
            try
            {
                return GetSubscriptions(_bySession, sessionId);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// Adds a subscription
        /// </summary>
        /// <param name="sessionId">The session to add the <paramref name="topic"/> to</param>
        /// <param name="topic">The topic to add to the <paramref name="sessionId"/></param>
        /// <param name="callback">The callback interface to use for notifications</param>
        public void Subscribe(string sessionId, string topic, INotification callback)
        {
            if (sessionId == null) throw new ArgumentNullException("sessionId");
            if (callback == null) throw new ArgumentNullException("callback");
            if (topic == null) topic = "";
            var subscription = new Subscription(topic, callback);

            bool addedTopic;
            _lock.EnterWriteLock();
            try
            {
                AddSubscription(_bySession, sessionId, subscription);
                addedTopic = AddSubscription(_byTopic, topic, subscription);
            }
            finally
            {
                _lock.ExitWriteLock();
            }

            // raise events outside of lock
            if (addedTopic)
            {
                InvokeTopicAdded(new SubscriptionEventArgs(topic));
            }
        }

        /// <summary>
        /// Remove a subscription
        /// </summary>
        /// <param name="sessionId">The session to remove the <paramref name="topic"/> from</param>
        /// <param name="topic">The topic to remove from the <paramref name="sessionId"/></param>
        public void Unsubscribe(string sessionId, string topic)
        {
            if (sessionId == null) throw new ArgumentNullException("sessionId");
            if (topic == null) topic = "";
            var subscription = new Subscription(topic, null);

            bool removedTopic;
            _lock.EnterWriteLock();
            try
            {
                RemoveSubscription(_bySession, sessionId, subscription);
                removedTopic = RemoveSubscription(_byTopic, topic, subscription);
            }
            finally
            {
                _lock.ExitWriteLock();
            }

            // raise events outside of lock
            if (removedTopic)
            {
                InvokeTopicRemoved(new SubscriptionEventArgs(topic));
            }
        }

        /// <summary>
        /// A session has ended, remove the session and all subscriptions
        /// </summary>
        /// <param name="sessionId"></param>
        public void UnsubscribeSession(string sessionId)
        {
            var topics = new List<string>();
            _lock.EnterUpgradeableReadLock();
            try
            {
                HashSet<Subscription> subscriptions;
                if (!_bySession.TryGetValue(sessionId, out subscriptions)) return;  // no session

                _lock.EnterWriteLock();
                try
                {
                    _bySession.Remove(sessionId);
                    topics.AddRange(from subscription in subscriptions
                                    where RemoveSubscription(_byTopic, subscription.Topic, subscription)
                                    select subscription.Topic);
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();                
            }

            // raise events outside of lock
            foreach (var topic in topics)
            {
                InvokeTopicRemoved(new SubscriptionEventArgs(topic));
            }
        }

        private void InvokeTopicAdded(SubscriptionEventArgs e)
        {
            var handler = TopicAdded;
            if (handler != null) handler(this, e);
        }
        
        private void InvokeTopicRemoved(SubscriptionEventArgs e)
        {
            var handler = TopicRemoved;
            if (handler != null) handler(this, e);
        }

        private static HashSet<Subscription> GetSubscriptions(IDictionary<string, HashSet<Subscription>> dictionary, string key)
        {
            if (key == null) key = "";
            HashSet<Subscription> subscriptions;
            dictionary.TryGetValue(key, out subscriptions);
            return subscriptions ?? new HashSet<Subscription>(); // safe to return as modifications use copy-modify-replace pattern
        }

        /// <remarks>
        /// use copy-modify-replace pattern so that returning subscriptions is thread-safe without using a lock
        /// </remarks>
        private static bool AddSubscription(Dictionary<string, HashSet<Subscription>> dictionary, string key, Subscription subscription)
        {
            bool firstForKey;
            HashSet<Subscription> subscriptions;
            HashSet<Subscription> copy;
            if (dictionary.TryGetValue(key, out subscriptions))
            {
                // take a copy of exist subscription before modifying it - for thread safety when being read
                copy = new HashSet<Subscription>(subscriptions);
                firstForKey = false;
            }
            else
            {
                // first subscription for this key
                copy = new HashSet<Subscription>();
                firstForKey = true;
                
            }
            copy.Add(subscription);
            dictionary[key] = copy;
            return firstForKey;
        }

        /// <remarks>
        /// use copy-modify-replace pattern so that returning subscriptions is thread-safe without using a lock
        /// </remarks>
        private static bool RemoveSubscription(IDictionary<string, HashSet<Subscription>> dictionary, string key, Subscription subscription)
        {
            bool lastForKey;
            HashSet<Subscription> subscriptions;
            if (!dictionary.TryGetValue(key, out subscriptions)) return false;

            var copy = new HashSet<Subscription>(subscriptions);
            copy.Remove(subscription);
            if (copy.Count > 0)
            {
                // replace the subscriptions
                dictionary[key] = copy;
                lastForKey = false;
            }
            else
            {
                // no more subscriptions for this key, remove the session
                dictionary.Remove(key);
                lastForKey = true;
            }
            return lastForKey;
        }
    }
}